/**
 *    Copyright 2009-2018 the original author or authors.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
package com.yinsin.jpabatis.executor;

import java.sql.SQLException;
import java.util.List;

import javax.persistence.EntityManager;

import org.springframework.data.domain.Page;

import com.yinsin.jpabatis.cache.Cache;
import com.yinsin.jpabatis.cache.CacheKey;
import com.yinsin.jpabatis.cache.TransactionalCacheManager;
import com.yinsin.jpabatis.mapper.BoundSql;
import com.yinsin.jpabatis.mapper.MappedStatement;
import com.yinsin.jpabatis.mapper.ParameterMapping;
import com.yinsin.jpabatis.mapper.ParameterMode;
import com.yinsin.jpabatis.mapper.StatementType;
import com.yinsin.jpabatis.session.ResultHandler;
import com.yinsin.jpabatis.session.RowBounds;

/**
 * @author Clinton Begin
 * @author Eduardo Macarron
 */
public class CachingExecutor implements Executor {

	private final Executor delegate;
	private final TransactionalCacheManager tcm = new TransactionalCacheManager();
	protected EntityManager transaction;

	public CachingExecutor(EntityManager transaction, Executor delegate) {
		this.transaction = transaction;
		this.delegate = delegate;
		delegate.setExecutorWrapper(this);
	}

	@Override
	public boolean isClose() {
		return delegate.isClose();
	}

	@Override
	public <E> E query(MappedStatement ms, Object parameterObject, ResultHandler<?> resultHandler) throws SQLException {
		BoundSql boundSql = ms.getBoundSql(parameterObject);
		CacheKey key = createCacheKey(ms, parameterObject, RowBounds.DEFAULT, boundSql);
		return query(ms, parameterObject, resultHandler, key, boundSql);
	}

	@Override
	public <E> E query(MappedStatement ms, Object parameterObject, ResultHandler<?> resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
		Cache cache = ms.getCache();
		if (cache != null) {
			flushCacheIfRequired(ms);
			if (ms.isUseCache() && resultHandler == null) {
				ensureNoOutParams(ms, boundSql);
				@SuppressWarnings("unchecked")
				E obj = (E) tcm.getObject(cache, key);
				if (obj == null) {
					obj = delegate.doQuery(ms, parameterObject, resultHandler, boundSql);
					tcm.putObject(cache, key, obj); // issue #578 and #116
				}
				return obj;
			}
		}
		return delegate.doQuery(ms, parameterObject, resultHandler, boundSql);
	}

	@Override
	public <E> List<E> queryList(MappedStatement ms, Object parameterObject, ResultHandler<?> resultHandler) throws SQLException {
		BoundSql boundSql = ms.getBoundSql(parameterObject);
		CacheKey key = createCacheKey(ms, parameterObject, RowBounds.DEFAULT, boundSql);
		return queryList(ms, parameterObject, resultHandler, key, boundSql);
	}

	@Override
	public <E> List<E> queryList(MappedStatement ms, Object parameterObject, ResultHandler<?> resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
		Cache cache = ms.getCache();
		if (cache != null) {
			flushCacheIfRequired(ms);
			if (ms.isUseCache() && resultHandler == null) {
				ensureNoOutParams(ms, boundSql);
				@SuppressWarnings("unchecked")
				List<E> list = (List<E>) tcm.getObject(cache, key);
				if (list == null) {
					list = delegate.doQueryList(ms, parameterObject, resultHandler, boundSql);
					tcm.putObject(cache, key, list); // issue #578 and #116
				}
				return list;
			}
		}
		return delegate.doQueryList(ms, parameterObject, resultHandler, boundSql);
	}
	
	@Override
	public <E> Page<E> queryList(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler<?> resultHandler) throws SQLException {
		BoundSql boundSql = ms.getBoundSql(parameterObject);
		CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
		return queryList(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
	}

	@Override
	public <E> Page<E> queryList(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler<?> resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
		Cache cache = ms.getCache();
		if (cache != null) {
			flushCacheIfRequired(ms);
			if (ms.isUseCache() && resultHandler == null) {
				ensureNoOutParams(ms, boundSql);
				@SuppressWarnings("unchecked")
				Page<E> list = (Page<E>) tcm.getObject(cache, key);
				if (list == null) {
					list = delegate.doQueryList(ms, parameterObject, rowBounds, resultHandler, boundSql);
					tcm.putObject(cache, key, list); // issue #578 and #116
				}
				return list;
			}
		}
		return delegate.doQueryList(ms, parameterObject, rowBounds, resultHandler, boundSql);
	}

	private void ensureNoOutParams(MappedStatement ms, BoundSql boundSql) {
		if (ms.getStatementType() == StatementType.CALLABLE) {
			for (ParameterMapping parameterMapping : boundSql.getParameterMappings()) {
				if (parameterMapping.getMode() != ParameterMode.IN) {
					throw new ExecutorException("Caching stored procedures with OUT params is not supported.  Please configure useCache=false in " + ms.getId() + " statement.");
				}
			}
		}
	}

	@Override
	public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
		return delegate.createCacheKey(ms, parameterObject, rowBounds, boundSql);
	}

	@Override
	public boolean isCached(MappedStatement ms, CacheKey key) {
		return delegate.isCached(ms, key);
	}

	@Override
	public void clearLocalCache() {
		delegate.clearLocalCache();
	}

	private void flushCacheIfRequired(MappedStatement ms) {
		Cache cache = ms.getCache();
		if (cache != null && ms.isFlushCacheRequired()) {
			tcm.clear(cache);
		}
	}

	@Override
	public void setExecutorWrapper(Executor executor) {
		throw new UnsupportedOperationException("This method should not be called");
	}

	@Override
	public EntityManager getTransaction() {
		return transaction;
	}

	@Override
	public <E> E doQuery(MappedStatement ms, Object parameter, ResultHandler<?> resultHandler, BoundSql boundSql) throws SQLException {
		return delegate.doQuery(ms, parameter, resultHandler, boundSql);
	}

	@Override
	public <E> List<E> doQueryList(MappedStatement ms, Object parameter, ResultHandler<?> resultHandler, BoundSql boundSql) throws SQLException {
		return delegate.doQueryList(ms, parameter, resultHandler, boundSql);
	}
	
	@Override
	public <E> Page<E> doQueryList(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler<?> resultHandler, BoundSql boundSql) throws SQLException {
		return delegate.doQueryList(ms, parameter, rowBounds, resultHandler, boundSql);
	}

}
